/* ===============================================================================  
 * Copyright (c) 2010 Acid Frameworks
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 * 
 * =============================================================================== */

using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading;
using System.Windows.Forms;
using Acid.Multimedia.DirectShow;

namespace Acid.Multimedia.Video
{
    public class CameraDevice : ISampleGrabberCB, IDisposable
    {
        private Control          control;
        private IFilterGraph2    filterGraph;
        private IAMVideoControl  videoControl;
        private IPin             pinStill;
        private ManualResetEvent pictureReadyEvent;
        private byte          [] videodata;
        private IntPtr           videodataPtr;
        
        private int    videoWidth;
        private int    videoHeight;
        private int    videoStride;

        private uint frametick = 0;
        
        public CameraDevice(Control control, DsDevice dsDevice, int iWidth, int iHeight, short iBPP)
        {
            this.control = control;
            
            try
            {
                // Set up the capture graph
                SetupGraph(dsDevice, iWidth, iHeight, iBPP);

                // tell the callback to ignore new images
                pictureReadyEvent = new ManualResetEvent(false);
            }
            catch
            {
                Dispose();

                throw;
            }

            this.videodata = new byte[System.Math.Abs(videoStride) * videoHeight];

            GCHandle temp = GCHandle.Alloc(this.videodata, GCHandleType.Pinned);

            this.videodataPtr = temp.AddrOfPinnedObject();
        }

        #region Properties

        public int Width
        {
            get
            {
                return videoWidth;
            }
        }
        public int Height
        {
            get
            {
                return videoHeight;
            }
        }
        public int Stride
        {
            get
            {
                return videoStride;
            }
        }

        #endregion

        

        


        public byte [] VideoData
        {
            get { return this.videodata; }
        }

        public void Update()
        {
            int hr;
            
            // get ready to wait for new image
            pictureReadyEvent.Reset();

            // If we are using a still pin, ask for a picture
            if (videoControl != null)
            {
                // Tell the camera to send an image
                hr = videoControl.SetMode(pinStill, VideoControlFlags.Trigger);

                DsError.ThrowExceptionForHR(hr);
            }
        }
        public void UpdateWait()
        {
            int hr;

            uint waithandle = this.frametick;

            pictureReadyEvent.Reset();

            while (waithandle == this.frametick) { }

            if (videoControl != null)
            {
                // Tell the camera to send an image
                hr = videoControl.SetMode(pinStill, VideoControlFlags.Trigger);

                DsError.ThrowExceptionForHR(hr);
            }
        }
        


        #region Members

        private void SetupGraph(DsDevice dev, int iWidth, int iHeight, short iBPP)
        {
            int hr;
            ISampleGrabber sampGrabber = null;
            IBaseFilter    capFilter = null;
            IPin           pCaptureOut = null;
            IPin           pSampleIn = null;
            IPin           pRenderIn = null;

            // Get the graphbuilder object
            this.filterGraph = new FilterGraph() as IFilterGraph2;

            try
            {
                // add the video input device
                hr = filterGraph.AddSourceFilterForMoniker(dev.Mon, null, dev.Name, out capFilter);
                DsError.ThrowExceptionForHR(hr);

                // Find the still pin
                pinStill = DsFindPin.ByCategory(capFilter, PinCategory.Still, 0);

                // Didn't find one.  Is there a preview pin?
                if (pinStill == null)
                {
                    pinStill = DsFindPin.ByCategory(capFilter, PinCategory.Preview, 0);
                }

                // Still haven't found one.  Need to put a splitter in so we have
                // one stream to capture the bitmap from, and one to display.  Ok, we
                // don't *have* to do it that way, but we are going to anyway.
                if (pinStill == null)
                {
                    IPin pRaw = null;
                    IPin pSmart = null;

                    // There is no still pin
                    videoControl = null;

                    // Add a splitter
                    IBaseFilter iSmartTee = (IBaseFilter)new SmartTee();

                    hr = filterGraph.AddFilter(iSmartTee, "SmartTee");
                    DsError.ThrowExceptionForHR(hr);

                    // Find the find the capture pin from the video device and the
                    // input pin for the splitter, and connnect them
                    pRaw = DsFindPin.ByCategory(capFilter, PinCategory.Capture, 0);
                    pSmart = DsFindPin.ByDirection(iSmartTee, PinDirection.Input, 0);

                    hr = filterGraph.Connect(pRaw, pSmart);
                    DsError.ThrowExceptionForHR(hr);

                    // Now set the capture and still pins (from the splitter)
                    pinStill = DsFindPin.ByName(iSmartTee, "Preview");
                    pCaptureOut = DsFindPin.ByName(iSmartTee, "Capture");

                    // If any of the default config items are set, perform the config
                    // on the actual video device (rather than the splitter)
                    if (iHeight + iWidth + iBPP > 0)
                    {
                        SetConfigParms(pRaw, iWidth, iHeight, iBPP);
                    }
                    
                    if (pRaw != null)     Marshal.ReleaseComObject(pRaw);
                    if (pRaw != pSmart)    Marshal.ReleaseComObject(pSmart);
                    if (pRaw != iSmartTee) Marshal.ReleaseComObject(iSmartTee);
                }
                else
                {
                    // Get a control pointer (used in Click())
                    videoControl = capFilter as IAMVideoControl;

                    pCaptureOut = DsFindPin.ByCategory(capFilter, PinCategory.Capture, 0);

                    // If any of the default config items are set
                    if (iHeight + iWidth + iBPP > 0)
                    {
                        SetConfigParms(pinStill, iWidth, iHeight, iBPP);
                    }
                }

                // Get the SampleGrabber interface
                sampGrabber = new SampleGrabber() as ISampleGrabber;

                // Configure the sample grabber
                IBaseFilter baseGrabFlt = sampGrabber as IBaseFilter;
                ConfigureSampleGrabber(sampGrabber);
                pSampleIn = DsFindPin.ByDirection(baseGrabFlt, PinDirection.Input, 0);

                // Get the default video renderer
                IBaseFilter pRenderer = new VideoRendererDefault() as IBaseFilter;
                hr = filterGraph.AddFilter(pRenderer, "Renderer");
                DsError.ThrowExceptionForHR(hr);

                pRenderIn = DsFindPin.ByDirection(pRenderer, PinDirection.Input, 0);

                // Add the sample grabber to the graph
                hr = filterGraph.AddFilter(baseGrabFlt, "Ds.NET Grabber");
                DsError.ThrowExceptionForHR(hr);

                if (videoControl == null)
                {
                    // Connect the Still pin to the sample grabber
                    hr = filterGraph.Connect(pinStill, pSampleIn);
                    DsError.ThrowExceptionForHR(hr);
                    
                    // Connect the capture pin to the renderer
                    hr = filterGraph.Connect(pCaptureOut, pRenderIn);
                    DsError.ThrowExceptionForHR(hr);
                }
                else
                {
                    // Connect the capture pin to the renderer
                    hr = filterGraph.Connect(pCaptureOut, pRenderIn);
                    DsError.ThrowExceptionForHR(hr);

                    // Connect the Still pin to the sample grabber
                    hr = filterGraph.Connect(pinStill, pSampleIn);
                    DsError.ThrowExceptionForHR(hr);
                }

                // Setup up the Video Dimentions
                GetVideoDimentions(sampGrabber);

                // Get the Video Window, dont Bind it to any Controls
                // Lives outside the main application frame. top left corner.
                this.SetupVideoWindow();

                // Start the graph
                IMediaControl mediaCtrl = filterGraph as IMediaControl;
                hr = mediaCtrl.Run();
                DsError.ThrowExceptionForHR(hr);
            }
            finally
            {
                if (sampGrabber != null)
                {
                    Marshal.ReleaseComObject(sampGrabber);
                    sampGrabber = null;
                }
                if (pCaptureOut != null)
                {
                    Marshal.ReleaseComObject(pCaptureOut);
                    pCaptureOut = null;
                }
                if (pRenderIn != null)
                {
                    Marshal.ReleaseComObject(pRenderIn);
                    pRenderIn = null;
                }
                if (pSampleIn != null)
                {
                    Marshal.ReleaseComObject(pSampleIn);
                    pSampleIn = null;
                }
            }
        }

        private void SetupVideoWindow()
        {
            int hr;
            IVideoWindow ivw = filterGraph as IVideoWindow;
            hr = ivw.put_WindowStyle(WindowStyle.Visible);
            DsError.ThrowExceptionForHR(hr);
            hr = ivw.put_Owner(this.control.Handle);
            hr = ivw.SetWindowPosition(0, 0, 0, 0);
            DsError.ThrowExceptionForHR(hr);
        }

        private void GetVideoDimentions(ISampleGrabber sampleGrabber)
        {
            int hr;
            
            AMMediaType media = new AMMediaType();
            hr = sampleGrabber.GetConnectedMediaType(media);
            DsError.ThrowExceptionForHR(hr);

            if ((media.formatType != FormatType.VideoInfo) || (media.formatPtr == IntPtr.Zero))
            {
                throw new NotSupportedException("Unknown Grabber Media Format");
            }

            // Grab the size info
            VideoInfoHeader videoInfoHeader = (VideoInfoHeader)Marshal.PtrToStructure(media.formatPtr, typeof(VideoInfoHeader));
            videoWidth  = videoInfoHeader.BmiHeader.Width;
            videoHeight = videoInfoHeader.BmiHeader.Height;
            videoStride     = videoWidth * (videoInfoHeader.BmiHeader.BitCount / 8);

            DsUtils.FreeAMMediaType(media);
            media = null;
        }

        private void ConfigureSampleGrabber(ISampleGrabber sampGrabber)
        {
            int hr;
            AMMediaType media = new AMMediaType();

            // Set the media type to Video/RGB24
            media.majorType = MediaType.Video;
            media.subType = MediaSubType.RGB24;
            media.formatType = FormatType.VideoInfo;
            hr = sampGrabber.SetMediaType(media);
            DsError.ThrowExceptionForHR(hr);

            DsUtils.FreeAMMediaType(media);
            media = null;

            // Configure the samplegrabber
            hr = sampGrabber.SetCallback(this, 1);
            DsError.ThrowExceptionForHR(hr);
        }
        
        private void SetConfigParms(IPin pStill, int iWidth, int iHeight, short iBPP)
        {
            int hr;
            AMMediaType media;
            VideoInfoHeader v;

            IAMStreamConfig videoStreamConfig = pStill as IAMStreamConfig;

            // Get the existing format block
            hr = videoStreamConfig.GetFormat(out media);
            DsError.ThrowExceptionForHR(hr);

            try
            {
                // copy out the videoinfoheader
                v = new VideoInfoHeader();
                Marshal.PtrToStructure(media.formatPtr, v);

                // if overriding the width, set the width
                if (iWidth > 0)
                {
                    v.BmiHeader.Width = iWidth;
                }

                // if overriding the Height, set the Height
                if (iHeight > 0)
                {
                    v.BmiHeader.Height = iHeight;
                }

                // if overriding the bits per pixel
                if (iBPP > 0)
                {
                    v.BmiHeader.BitCount = iBPP;
                }

                // Copy the media structure back
                Marshal.StructureToPtr(v, media.formatPtr, false);

                // Set the new format
                hr = videoStreamConfig.SetFormat(media);
                DsError.ThrowExceptionForHR(hr);
            }
            finally
            {
                DsUtils.FreeAMMediaType(media);
               
                media = null;
            }
        }

        private void CloseInterfaces()
        {
            int hr;

            try
            {
                if (filterGraph != null)
                {
                    IMediaControl mediaCtrl = filterGraph as IMediaControl;

                    hr = mediaCtrl.Stop();
                }
            }
            catch (Exception ex)
            {
                Debug.WriteLine(ex);
            }

            if (filterGraph != null)
            {
                Marshal.ReleaseComObject(filterGraph);
                filterGraph = null;
            }

            if (videoControl != null)
            {
                Marshal.ReleaseComObject(videoControl);
                videoControl = null;
            }

            if (pinStill != null)
            {
                Marshal.ReleaseComObject(pinStill);
                pinStill = null;
            }
        }

        #endregion

        #region IDisposible Members

        

       private bool disposed = false;

        private void Dispose(bool disposing)
        {
            if (!disposed)
            {
                if (disposing)
                {

                }

                CloseInterfaces();

                if (pictureReadyEvent != null)
                {
                    pictureReadyEvent.Close();
                }
                
                this.disposed = true;
            }
        }

        public void Dispose()
        {
            this.Dispose(true);

            GC.SuppressFinalize(this);
        }

        ~CameraDevice()
        {
            this.Dispose(false);
        }


        #endregion

        #region ISampleGrabberCB Members

        int ISampleGrabberCB.SampleCB(double SampleTime, IMediaSample pSample)
        {
            Marshal.ReleaseComObject(pSample);
            
            return 0;
        }

        int ISampleGrabberCB.BufferCB(double SampleTime, IntPtr pBuffer, int BufferLen)
        {
            CopyMemory(this.videodataPtr, pBuffer, BufferLen);

            pictureReadyEvent.Set();

            // Used to wait on the current frame. could be anything.
            this.frametick++; if (this.frametick > uint.MaxValue) this.frametick = uint.MinValue;

            return 0;
        }

        #endregion

        #region External
        [DllImport("Kernel32.dll", EntryPoint = "RtlMoveMemory")]
        private static extern void CopyMemory(IntPtr Destination, IntPtr Source, [MarshalAs(UnmanagedType.U4)] int Length);
        #endregion

      
    }
}
